perf: fast digests for user-signed actions, leaner ws/http per-request paths - #86
Merged
Merged
Conversation
…t paths
User-signed actions (approveAgent, usdSend, and the other 15) no longer pay
viem's generic hashTypedData. The EIP-712 domain is fixed and every shipped
types object is a module constant, so typehash plans compile once per types
identity (WeakMap) and domain separators once per chainId, following the
_fastDigest pattern used for Agent/SendMultiSig. Signing routes through the
raw-digest capability (WASM when present); remote and ledger wallets fall
back to signTypedData unchanged. Digests are byte-identical to viem's
hashTypedData across all 17 types x 5 chain IDs (userSignedDigest.test.ts).
Measured: 2.5 us vs 56 us per digest; multisig_user_signed_3_signers -22%.
Multi-sig computes the shared digest once per call and only after a
capability check, so stub/remote signers never pay for a digest they
cannot use.
Websocket transport:
- The dispatcher schedules request timeouts on the shared abort.TimeoutWheel
(as HttpTransport already did) instead of a native setTimeout per request:
ws_request_round_trip -10%.
- Routing stops allocating per frame: .toLowerCase() is gated behind an
uppercase check (the server sends lowercase hex), and routed event-type
strings are interned per channel+key instead of re-concatenated:
webData3_frame_dispatch_e2e -38%, l2book_dispatch_50_coins -28%.
- WebSocketTransport.request drops a pointless async/return await.
Subscriptions and utils:
- fastAssetCtxs decodes the node:zlib Buffer via Buffer.toString("utf8")
instead of a shared TextDecoder on the hottest decode path; the
DecompressionStream fallback fuses its two .then hops into one.
- floatToWire renders the double once (toFixed(9)) and derives the 8-decimal
wire string by digit-9 rounding with carry: -20%, fuzz-verified
byte-identical over 1.8M values.
Http transport:
- Error paths skip the redaction walk for signature-free payloads (all info
requests), gated on a wire-string check.
- No AbortController or TimeoutWheel entry is allocated when timeout is null
and no signal exists; the wheel hands out a shared frozen null-handle.
- Explorer requests skip the pre-send JSON.parse (flat weight needs no
parsed form); billing snapshots materialize lazily for surcharge/error
paths only. Info/exchange billing still parses the wire form — a pinned
test contract requires billing to derive from the serialized payload.
Exchange shell:
- Signing moves outside the per-wallet nonce lock; wire order is preserved
by a per-(wallet x network) dispatch chain instead, so concurrent callers
on one wallet (where signing is a network round trip for any remote
wallet) sign in parallel while the server still sees strictly increasing
nonces. Covered by the new _dispatchOrder tests.
- extractNonceFieldName and static signatureChainId validation are memoized
per types/config identity; the multi-sig inner hash no longer round-trips
through hex; msgpack uint64 writes integers without BigInt boxing.
Perf harness:
- subscribe_user_trio stabilised (from the abandoned
perf/stabilize-subscribe-user-trio branch): 20 iterations per sample
instead of 1, and MockWebSocket keeps only the latest instance instead of
retaining every socket. Run-to-run spread falls from 33% to ~3%, stopping
the scenario flipping the gate on unrelated PRs.
- New scenarios: eip712_user_signed_digest (+viem oracle pair),
approve_agent_e2e_no_ecdsa. Baseline re-recorded (the committed one was
stale since 0.1.4 and failed the local gate closed).
Full suite vs old baseline: 11 faster, 0 regressed. perf:gate PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything on
mainthat had not yet reachedorigin. This was committed locally asbbfc7ecand is being routed through a PR so it gets CI before any release is cut from it.Contents
Signing. User-signed actions (
approveAgent,usdSend, and the other 15) no longer pay viem's generichashTypedData. Typehash plans compile once per types identity and domain separators once per chainId, following the_fastDigestpattern already used for Agent/SendMultiSig. Digests are byte-identical to viem's across all 17 types × 5 chain IDs (userSignedDigest.test.ts). Measured 2.5 µs vs 56 µs per digest;multisig_user_signed_3_signers−22%.Order path — the lock hoist.
executeWithShellnow holds the nonce lock across only nonce issuance and a dispatch-chain claim; signing moved outside it, so concurrent callers on one wallet sign simultaneously. Wire order is preserved by a per-(wallet × network)dispatch chain rather than by the lock.Measured with a remote wallet, concurrent
approveAgent:eth_accountseth_chainIdsignTypedDataLatency is constant in N, bounded by the data-dependency floor. Previously N=20 cost 892–3760 ms with 20
eth_chainIdcalls. The hoist also incidentally collapses the redundant chain-id and multi-sig leader-address round trips, becausebuildnow runs outside the lock and their dedupe caches can finally fire.Ordering is covered by
tests/api/exchange/_dispatchOrder.test.ts: signatures completing in reverse order, jittered latency, and signing failures at the first/middle/last position — a burned nonce must leave a gap without stalling later requests or letting them overtake an earlier nonce still being signed. The suite was verified to have teeth by re-injecting that exact bug.Websocket / HTTP. Dispatcher timeouts moved to the shared
abort.TimeoutWheel(ws_request_round_trip−10%); routing stops allocating per frame —toLowerCase()gated behind an uppercase check, routed event-type strings interned per channel+key (webData3_frame_dispatch_e2e−38%,l2book_dispatch_50_coins−28%).Perf harness.
subscribe_user_triostabilised (absorbed fromperf/stabilize-subscribe-user-trio, which #85 tracked): run-to-run spread 33% → ~3%, so it stops flipping the gate on unrelated PRs. Baseline re-recorded — the committed one was stale since 0.1.4 and failed the local gate closed.Full suite vs old baseline: 11 faster, 0 regressed,
perf:gatePASS.Note
The
perfcheck will likely go red on the suite-fingerprint guard, since this adds scenarios and re-records the baseline — the same fail-closed path as #83, which the gate's own message sanctions for an intentional suite change.🤖 Generated with Claude Code